package com.carrot.gateway.filter.handler.auth;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.json.JSONObject;
import com.carrot.common.enums.ErrorCodeEnum;
import com.carrot.common.utils.AESUtils;
import com.carrot.gateway.constant.RedisConstants;
import com.carrot.gateway.enums.GatewayExceptionEnum;
import com.carrot.gateway.property.CustomSecurityProperties;
import com.carrot.gateway.util.GetPostBodyUtil;
import com.carrot.gateway.util.IpAddressUtil;
import com.carrot.gateway.util.RedisUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

/**
 * v1及按照规则未解析到api版本
 */
@Component
@Slf4j
public class AuthHandler extends AbstractAuthHandler {

    private final CustomSecurityProperties customSecurityProperties;

    public final static String SIGN_HEADER = "CIRCUE-AIGO-Sign";
    private final static String API_SYMBOL = "/api/";
    private final static String SPECIAL_URL = "/tenant-biz/api/v1/message/query-unread-count";

    @Override
    public Mono<Void> authCheck(ServerWebExchange exchange, GatewayFilterChain chain, String serviceName) {
        ServerHttpRequest sourceRequest = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        //1. 检查ip是否在黑名单中
        String remoteIp = IpAddressUtil.getClientIpAddress(sourceRequest);
        if(CollectionUtil.isNotEmpty(customSecurityProperties.getBlackIpList())
                && customSecurityProperties.getBlackIpList().contains(remoteIp)){
            log.info("ip被禁止访问 ip:{}", remoteIp);
            return errorResult(response, GatewayExceptionEnum.IP_PROHIBITION);
        }

        //2. 访问url在白名单中直接放行
        String path = sourceRequest.getPath().toString();
        //白名单和OPTIONS放行
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        if (CollectionUtil.isNotEmpty(customSecurityProperties.getIgnoreUriList())
                && customSecurityProperties.getIgnoreUriList().stream().anyMatch(uri -> antPathMatcher.match(uri, path))) {
            return chain.filter(exchange);
        }

        //3. 校验 access token(token在缓存中已失效则返回错误)
        HttpHeaders headers = sourceRequest.getHeaders();
        String token = handleToken(headers, HttpHeaders.AUTHORIZATION);
        String accessTokenKey= RedisUtils.getUserOnLineAccessTokenKey(token);
        Boolean exist = redisTemplate.hasKey(accessTokenKey);
        //token失效返回错误
        if(Boolean.FALSE.equals(exist)){
            //单独对 tenant-biz/api/v1/message/query-unread-count接口 做处理，后期需优化 TODO
/*            if(SPECIAL_URL.equals(path)){
                return errorResult(response, GatewayExceptionEnum.AUTH_FAIL);
            }*/
            return errorResult(response, ErrorCodeEnum.TOKEN_EXPIRE);
        }

        //4. 获取签名并校验请求参数签名是否合法
        String requestSign = headers.getFirst(SIGN_HEADER);
        if(StringUtils.isBlank(requestSign)){
            return errorResult(response, GatewayExceptionEnum.SIGN_INVALID);
        }
        final Map<String, String> requestSingMap = this.parserSign(requestSign);
        if(requestSingMap == null){
            log.info("sign头解析失败 requestSign:{}", requestSign);
            return errorResult(response, GatewayExceptionEnum.SIGN_INVALID);
        }
        final String requestBody = GetPostBodyUtil.resolveBodyFromRequest(sourceRequest);
        if(!this.checkSign(requestSingMap, requestBody, token)){
            return errorResult(response, GatewayExceptionEnum.SIGN_INVALID);
        }

        //5. 校验请求的api是否有权限
        final JSONObject jsonUserInfo = getUserInfoForRedis(accessTokenKey);
        String apiCacheKey = RedisConstants.ORDER_API_SERVICE_KEY
                + jsonUserInfo.getStr("tenantId")
                + RedisConstants.SEPARATOR
                + serviceName;
        String api = path.substring(path.indexOf(API_SYMBOL) + API_SYMBOL.length());
        final Boolean isApiExist = redisTemplate.opsForSet().isMember(apiCacheKey, api);
        if(isApiExist == null || Boolean.FALSE.equals(isApiExist)){
            return errorResult(response, GatewayExceptionEnum.API_NO_ACCESS_PERMISSION);
        }

        //更换请求头中的AUTHORIZATION
        return chain.filter(
                exchange.mutate().request(
                    sourceRequest.mutate().headers(k -> k.remove(HttpHeaders.AUTHORIZATION))
                        .header(HttpHeaders.AUTHORIZATION, jwtUserInfo(jsonUserInfo)).build()
                ).build()
        );

    }

    /**
     * 将请求的sign头转换为map
     * @param requestSign
     * @return
     */
    private Map<String, String> parserSign(String requestSign){
        final String[] signs = requestSign.split(",");
        //按照规则sign头中必须有5个参数 nonceStr appId timestamp signType signature
        if(signs.length != 5){
            return null;
        }
        Map<String, String> result = new HashMap<>(5);
        for(String item : signs){
            if(!item.contains("=")){
                return null;
            }
            final int firstEqu = item.indexOf("=");
            result.put(item.substring(0, firstEqu).trim(), item.substring(firstEqu + 1).replaceAll("\"", "").trim());
        }
        return result;
    }

    /**
     * 校验签名
     * @param requestSignMap
     * @param requestBody
     * @return
     */
    private boolean checkSign(Map<String, String> requestSignMap, String requestBody, String accessToken){
        // 将参数放入 TreeMap 中按照字典序排序
        TreeMap<String, String> params = new TreeMap<>();
        params.put("accessToken", accessToken);
        params.put("appId", requestSignMap.get("appId"));
        params.put("timestamp", requestSignMap.get("timestamp"));
        params.put("nonceStr", requestSignMap.get("nonceStr"));
        params.put("signType", requestSignMap.get("signType"));
        params.put("body", requestBody);
        // 构建待签名字符串
        StringBuilder stringBuilder = new StringBuilder();
        for (Map.Entry<String, String> entry : params.entrySet()) {
            stringBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
        }
        String plaintext = stringBuilder.substring(0, stringBuilder.length() - 1);
        String requestSignValue = requestSignMap.get("signature");
        //获取app secret
        String clientInfoKey = RedisConstants.TENANT_CLIENT_INFO_KEY + requestSignMap.get("appId");
        Object clientSecretObj = null;
        try{
            clientSecretObj = redisTemplate.opsForHash().get(clientInfoKey, "client_secret");
        }catch (Exception e){
            log.error("获取租户的app secret发生异常 key:{}, hashKey:{}", clientInfoKey, "client_secret", e);
        }
        if(clientSecretObj == null){
            log.info("获取租户的app secret失败 key:{}, hashKey:{}", clientInfoKey, "client_secret");
            return false;
        }
        String ciphertext = "";
        try{
            final String clientSecret = AESUtils.decrypt(clientSecretObj.toString());
            final HMac hMac = new HMac(HmacAlgorithm.HmacSHA256, clientSecret.getBytes(StandardCharsets.UTF_8));
            ciphertext = hMac.digestBase64(plaintext, false);
        }catch (Exception e){
            log.error("签名验证发生异常 requestSignValue:{}", requestSignValue, e);
        }
        if(!ciphertext.equals(requestSignValue)){
            log.info("签名验证失败 plaintext:{}, requestSignValue:{}, ciphertext:{}", plaintext, requestSignValue, ciphertext);
            return false;
        }
        return true;
    }

    public AuthHandler(RedisTemplate<String, Object> redisTemplate,
                       ObjectMapper objectMapper,
                       CustomSecurityProperties customSecurityProperties) {
        this.customSecurityProperties = customSecurityProperties;
        this.redisTemplate = redisTemplate;
        this.objectMapper  = objectMapper;
    }
}
